package com.iogogogo.supervisord.core.rpc;

import com.iogogogo.supervisord.annotation.SupervisordEvent;
import com.iogogogo.supervisord.core.Supervisord;
import com.iogogogo.supervisord.domain.SupervisordProcess;
import com.iogogogo.supervisord.exception.SupervisordException;
import com.iogogogo.supervisord.properties.SupervisordProperties;
import com.iogogogo.supervisord.util.BeanMapUtils;
import com.iogogogo.supervisord.util.XmlParse;
import io.vavr.Tuple2;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.util.StringUtils;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Created by tao.zeng on 2021/6/30.
 */
@Slf4j
public class SupervisordRpc implements Supervisord {

    private final OkHttpClient httpClient;

    private final SupervisordProperties supervisordProperties;

    /**
     * Instantiates a new Supervisord rpc.
     *
     * @param supervisordProperties the supervisord properties
     */
    public SupervisordRpc(SupervisordProperties supervisordProperties) {
        this.supervisordProperties = supervisordProperties;
        this.httpClient = httpClient(supervisordProperties);
    }

    @Override
    public SupervisordProperties getSupervisordProperties() {
        return this.supervisordProperties;
    }

    @SupervisordEvent
    @Override
    public List<String> listMethods() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(SystemMethod._LIST_METHODS);
        Object[] call = XmlParse.parse(call(param));
        return Stream.of(call).map(String::valueOf).collect(Collectors.toList());
    }

    @SupervisordEvent
    @Override
    public String methodHelp(String method) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(SystemMethod._METHOD_HELP);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Object methodSignature(String name) throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object multicall(Map<String, Object> calls) throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public String getAPIVersion() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._GET_API_VERSION));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String getSupervisorVersion() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._GET_SUPERVISOR_VERSION));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String getIdentification() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._GET_IDENTIFICATION));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Map<String, Object> getState() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._GET_STATE));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Integer getPID() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._GET_PID));
        return XmlParse.parse(call(param));
    }

    /**
     * Read length bytes from the main log starting at offset
     * <p>
     * offset offset to start reading from. @param int length number of bytes to read from the log. @return string result Bytes of log
     * <p>
     * It can either return the entire log, a number of characters from the tail of the log, or a slice of the log specified by the offset and length parameters:
     *
     * @param offset offset to start reading from
     * @param length number of bytes to read from the log
     * @return string result Bytes of log
     * @throws IOException
     * @throws SupervisordException
     */
    @SupervisordEvent
    @Override
    public String readLog(int offset, int length) throws IOException, SupervisordException {
        if (offset < 0) offset = 0;
        if (length < 0) length = 0;
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._READ_LOG), offset, length);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Boolean clearLog() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._CLEAR_LOG));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Boolean restart() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._RESTART));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Object shutdown() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(StatusAndControl._SHUTDOWN));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public SupervisordProcess getProcessInfo(String procName) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._GET_PROCESS_INFO), procName);
        Map<String, Object> map = XmlParse.parse(call(param));
        return BeanMapUtils.mapToBean(map, SupervisordProcess.class);
    }

    @SuppressWarnings("unchecked")
    private List<SupervisordProcess> convert(Object[] call) {
        return Stream.of(call)
                .filter(Objects::nonNull)
                .filter(x -> x instanceof Map)
                .map(x -> (Map<String, Object>) x)
                .map(x -> BeanMapUtils.mapToBean(x, SupervisordProcess.class))
                .collect(Collectors.toList());
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> getAllProcessInfo() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._GET_ALL_PROCESS_INFO));
        return convert(XmlParse.parse(call(param)));
    }

    @SupervisordEvent
    @Override
    public Boolean startProcess(String processName, boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._START_PROCESS), processName, wait);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> startAllProcesses(boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._START_ALL_PROCESSES), wait);
        return convert(XmlParse.parse(call(param)));
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> startProcessGroup(String name, boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._START_PROCESS_GROUP), name, wait);
        return convert(XmlParse.parse(call(param)));
    }

    @SupervisordEvent
    @Override
    public Boolean stopProcess(String processName, boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._STOP_PROCESS), processName, wait);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> stopAllProcesses(boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._STOP_ALL_PROCESSES), wait);
        return convert(XmlParse.parse(call(param)));
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> stopProcessGroup(String name, boolean wait) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._STOP_PROCESS_GROUP), name, wait);
        return convert(XmlParse.parse(call(param)));
    }

    @SupervisordEvent
    @Override
    public Object signalProcess(String name, int signal) throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object signalAllProcesses(int signal) throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object signalProcessGroup(String name, int signal) throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object sendProcessStdin() throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object sendRemoteCommEvent() throws IOException, SupervisordException {
        return null;
    }

    @SupervisordEvent
    @Override
    public Object reloadConfig() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._RELOAD_CONFIG));
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Boolean addProcessGroup(String name) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._ADD_PROCESS_GROUP), name);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Boolean removeProcessGroup(String name) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessControl._REMOVE_PROCESS_GROUP), name);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String readProcessStdoutLog(String name, long offset, long length) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._READ_PROCESS_STDOUT_LOG), name, offset, length);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String readProcessStderrLog(String name, long offset, long length) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._READ_PROCESS_STDERR_LOG), name, offset, length);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String tailProcessStdoutLog(String name, long offset, long length) throws IOException, SupervisordException {
        if (offset < 0) offset = 0;
        if (length < 0) length = 0;
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._TAIL_PROCESS_STDOUT_LOG), name, offset, length);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public String tailProcessStderrLog(String name, long offset, long length) throws IOException, SupervisordException {
        if (offset < 0) offset = 0;
        if (length < 0) length = 0;
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._TAIL_PROCESS_STDERR_LOG), name, offset, length);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public Boolean clearProcessLogs(String name) throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._CLEAR_PROCESS_LOGS), name);
        return XmlParse.parse(call(param));
    }

    @SupervisordEvent
    @Override
    public List<SupervisordProcess> clearAllProcessLogs() throws IOException, SupervisordException {
        Tuple2<String, String> param = buildParam(buildFullParam(ProcessLogging._CLEAR_ALL_PROCESS_LOGS));
        return XmlParse.parse(call(param));
    }

    /**
     * Call string.
     *
     * @param param the param
     * @return the string
     * @throws IOException          the io exception
     * @throws SupervisordException the supervisord exception
     */
    private String call(Tuple2<String, String> param) throws IOException, SupervisordException {
        log.debug("supervisord param:\n{}", param);

        RequestBody requestBody = RequestBody
                .create(MediaType.parse(org.springframework.http.MediaType.APPLICATION_XML_VALUE),
                        param._2().getBytes(StandardCharsets.UTF_8));

        String url = supervisordProperties.getUrl();
        log.info("supervisord rpc: {}", url);

        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();

        Response response = httpClient.newCall(request).execute();

        log.info("supervisord {} response code: {}", param._1, response.code());

        ResponseBody responseBody = response.body();

        if (response.isSuccessful() && Objects.nonNull(responseBody)) {
            String resp = responseBody.string();
            log.debug("supervisord {} response: {}", param._1, resp);
            return resp;
        } else {
            throw new SupervisordException(response.message());
        }
    }

    public OkHttpClient httpClient(SupervisordProperties supervisordProperties) {
        SupervisordProperties.OkHttp okHttp = supervisordProperties.getOkHttp();

        String username = supervisordProperties.getUsername();
        String password = supervisordProperties.getPassword();

        OkHttpClient.Builder xBuilder = new OkHttpClient().newBuilder()
                .connectTimeout(okHttp.getConnectTimeout())
                .callTimeout(okHttp.getCallTimeout())
                .readTimeout(okHttp.getReadTimeout())
                .writeTimeout(okHttp.getWriteTimeout())
                .hostnameVerifier((hostName, session) -> true)
                .retryOnConnectionFailure(true)
                .addInterceptor(chain -> {
                    Request original = chain.request();
                    Request.Builder builder = original.newBuilder()
                            .header("User-Agent", "Mozilla/5.0")
                            .header("Accept-Language", "en-US,en;q=0.5")
                            .header("Accept", "*/*")
                            .header("Content-Type", org.springframework.http.MediaType.APPLICATION_XML_VALUE)
                            .method(original.method(), original.body());

                    if (StringUtils.hasLength(username)) {
                        String auth = new String(Base64.getEncoder().encode(String.join(":", username, password)
                                .getBytes(StandardCharsets.UTF_8)));
                        builder.header("Authorization", "Basic " + auth);
                        log.debug("auth: {}", username);
                    }

                    return chain.proceed(builder.build());
                });

        if (!supervisordProperties.isSslEnable()) {
            TrustManager[] trustManagers = buildTrustManagers();
            xBuilder.sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0]);
        }

        return xBuilder.build();
    }

    /**
     * 生成安全套接字工厂，用于https请求的证书跳过
     *
     * @param trustAllCerts
     * @return
     */
    private SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ssfFactory;
    }

    private TrustManager[] buildTrustManagers() {
        return new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };
    }
}
